
"""
CoordPlane, or CoordinatePlane
An infinite plane that contains objects at various locations. 
"""

import logging
from pgEngine.point import *

class CollisionError(Exception):
    """ Represents an attempt to move one object into another. """
    def __init__(self, collider, colliderLoc, collidee, collideeLoc):
        Exception.__init__(self)
        self.collider = collider
        self.colliderLoc = colliderLoc
        self.collidee = collidee
        self.collideeLoc = collideeLoc

#TODO: Write a Coordinate Plane that doesn't let items go outside a polygon

class CoordPlane(object):
    """
    Maintains the locations of game objects, and the landscape. Not simply
    the World class because there might be some World-specific feature that
    isn't implemented just yet.
    """
    # Location by object -> x,y are floats
    # items[<item>] => Point(x,y)
    items = {}
    # Object by location -> x,y are integers
    # locations[(x,y)] => item
    locations = {}
    game = None

    logger = logging.getLogger("engine.CoordPlane")

    def __new__(cls, itemLocs=None, game=None):
        if itemLocs is None: itemLocs = {}
        self = super(CoordPlane, cls).__new__(cls)
        for item, coord in itemLocs:
            self.items[item] = coord
            self.locations[coord] = item
        self.game = game
        return self

    def __str__(self):
        return '<CoordPlane(%i) at %s>' % (len(self.items), id(self))
    
    def __repr__(self):
        return 'CoordPlane(%r)' % (self.items)
    
    # Game Objects
    def moveItem(self, item, vector):
        """ Move an object by a relative vector. """
        if item not in self.items:
            raise ValueError, "Item does not yet exist in CoordPlane: %s" % item
        
        delta = Point.fromVector(vector.dir.angle, vector.dist)
        oldLoc = self.items[item]
        newLoc = oldLoc + delta
        if newLoc.loc in self.locations:
            collidee = self.locations[newLoc.loc]
            if collidee is not item:
                raise CollisionError(item, newLoc, collidee, 
                                     self.items[collidee])
        self.items[item] = newLoc
        del self.locations[oldLoc.loc]
        self.locations[newLoc.loc] = item
        
        return newLoc
        
    def moveItemTo(self, item, point):
        """ Move an item to a specific location. """
        if point.loc in self.locations:
            collidee = self.locations[point.loc]
            raise CollisionError(item, point,
                                 collidee, self.items[collidee])
        elif item in self.items:
            oldLoc = self.items[item]
            del self.locations[oldLoc.loc]
            
        self.items[item] = point
        self.locations[point.loc] = item
        
    def append(self, item, point):
        """ Add a non-pre-extant item. """
        if item in self.items:
            raise ValueError, "Item already exists in CoordPlane: %s" % item
        self.moveItemTo(item, point)

    def getCoord(self, item):
        """ Return the location of a specific object. """
        return self.items[item]

    def getNearestItem(self, source, targetType):
        least = None
        nearestItem = None
        sourceLoc = self.items[source]
        for item,loc in self.items.items():
            if type(item) is not targetType: continue
            dist = sourceLoc.dist(loc)
            if least is None or dist < least:
                nearestItem = item
                least = dist
        return nearestItem
    
    def getItems(self, x, y, w, h):
        x2 = x + w
        y2 = y + h
        items = [ (loc,item) for item,loc in self.items.items()
                             if loc.x >= x and loc.x <= x2 and
                                loc.y >= y and loc.y <= y2 ]
        return items

    def checkCollision(self, location):
        if location.loc in self.locations:
            collidee = self.locations[location.loc]
            return (collidee, self.items[collidee])
        return None
    
    def getDistance(self, source, target):
        p1, p2 = self.getCoord(source), self.getCoord(target)
        return ((p1.x-p2.x)**2 + (p1.y-p2.y)**2)**.5

class CoordPoly(CoordPlane):
    """
    A coordinate plane that ensures all of its contents stay inside the plane's bounds.
    Bounds are defined by a set of points given in order. 
    """
    bounds = None
    game = None
    items = None
    locations = None
    allowedType = object
    def __new__(cls, boundPoints, itemLocs=None, game=None, typeAllowed=object):
        if itemLocs is None: itemLocs = {}
        self = super(CoordPoly, cls).__new__(cls, itemLocs, game)
        self.bounds = BoundingPoly(boundPoints)
        self.allowedType = typeAllowed
        for loc, item in self.locations:
            if not self.bounds.collision(loc) or not isinstance(item, allowedType):
                #Remove the item.
                del self.locations[loc]
                del self.items[item]
        return self
    
    def __validate(self, item, location):
        """
        Ensure that the purposed change could work. Does not raise an exception.
        """
        return self.bounds.collision(location) and isinstance(item, self.allowedType)
    
    def moveItem(self, item, vector):
        delta = Point.fromVector(vector.dir.angle, vector.dist)
        oldLoc = self.items[item]
        newLoc = oldLoc + delta
        if not self.__validate(item, newLoc):
            return False
        
        if newLoc in self.locations:
            collidee = self.locations[newLoc]
            if collidee is not item:
                raise CollisionError(item, newLoc, collidee, 
                                     self.items[collidee])
        self.items[item] = newLoc
        del self.locations[oldLoc]
        self.locations[newLoc] = item
        
        return newLoc
    
    def moveItemTo(self, item, point):
        """ Move an item to a specific location. """
        if not self.__validate(item, point): return False
        if point in self.locations:
            collidee = self.locations[point]
            raise CollisionError(item, point,
                                 collidee, self.items[collidee])
        elif item in self.items:
            oldLoc = self.items[item]
            del self.locations[oldLoc]
            
        self.items[item] = point
        self.locations[point] = item
    
    def append(self, item, loc):
        """ Add a non-pre-extant item. """
        if item in self.items:
            raise ValueError, "Item already exists in CoordPlane: %s" % item
        self.moveItemTo(item, point)
    
    def getCoord(self, item):
        """ Return the location of a specific object. """
        return super(CoordPoly, self).getCoord(item)
    
    def getNearestItem(self, source, type):
        return super(CoordPoly, self).getNearestItem(source, type)
    
    def getItems(self, x, y, w, h):
        return super(CoordPoly, self).getItems(x, y, w, h)
    
    def checkCollision(self, location):
        return super(CoordPoly, self).checkCollision(location)
    
    def getDistance(self, source, target):
        return super(CoordPoly, self).getDistance(source, target)
    
    